缓存控制中的 stale-while-revalidate

April 24, 2020

stale-while-revalidateHTTP RFC 5861 中描述的一种 Cache-Control 扩展,属于对缓存到期的控制。

缓存控制

很多人都了解浏览器的缓存机制,这里简单温习一下。为了提高响应速度,浏览器会帮我们把一些请求的响应缓存下来,在下次请求相同的资源时,直接返回缓存的结果。但业务中有些资源是不应该缓存的,应该总是请求最新的结果,浏览器怎么判断哪些资源可以缓存,缓存多久这些信息呢?HTTP 缓存文档中允许服务端设置一些响应头 (如 Cache-control) 来告诉浏览器如何缓存这个响应。

Cache-Control: max-age=600

max-age 就是 Cache-control 包含的一个指令,用于设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。比如上面的例子,在 600 秒内,再次请求这个资源,浏览器就会返回缓存的响应,超过这个时间后,请求这个资源,浏览器就会请求新的结果,应用就需要等待这个请求。

含义

那我们回到 stale-while-revalidate=<seconds>。它表明客户端愿意接受陈旧 (stale) 的响应,同时在后台异步检查新的响应 (revalidate)。秒值指示客户愿意接受陈旧响应的时间长度。

怎么样的响应算是陈旧的响应?

stale-while-revalidate 指令应当与 max-age 配合使用,超过 max-age 指定的时间的响应就是陈旧的响应。与之相对的,没有超过时间的就是新鲜的 (fresh) 响应。如果一个缓存的响应没有超过 max-age 指定的时间(仍是新鲜的),按照上面讲的缓存机制,此时请求这个资源,浏览器会直接返回缓存的结果。如果缓存的结果已经陈旧了呢?按照前面讲的缓存机制,浏览器应该去请求新的响应了,但是如果存在 stale-while-revalidate 指令就不一样了,浏览器会检查这个陈旧的响应是否超过了 stale-while-revalidate 规定的时间窗口。如果没有超过,那么浏览器仍然会直接返回缓存的结果,同时在后台请求新的结果用来更新缓存。

有点绕…

我们看 RFC 5861 给出的示例用法。

用法

Cache-Control: max-age=600, stale-while-revalidate=30

这个响应头规定了缓存的周期为 600 秒,可接受 30 秒内的陈旧响应。如果在 600 秒之内请求这个资源,浏览器就会直接返回缓存的响应。如果在 600 秒之后请求,浏览器会检查是否已经超过可接受的陈旧时间,也就是总共是否超过了 630 秒。如果没有超过的话,仍然返回缓存的响应,同时在后台请求新的响应。如果超过了 630 秒,就直接请求新的响应,应用将等待这个请求。

这和直接设置 max-age=630 有什么不一样?

我们设想这样一个步骤,比较两种方式的异同。

设置 max-age=630

  1. 初次请求,应用等待请求,得到新鲜的响应,存入缓存
  2. 在 600 秒内再次请求,不用等,得到缓存响应
  3. 在 610 秒时再次请求,不用等,得到缓存响应
  4. 在 640 秒时再次请求,应用等待请求,得到新鲜的响应,存入缓存

设置 max-age=600, stale-while-revalidate=30

  1. 初次请求,应用等待请求,得到新鲜响应,存入缓存
  2. 在 600 秒内再次请求,不用等,得到缓存响应
  3. 在 610 秒时再次请求,不用等,得到缓存响应,同时后台请求了新的响应,存入缓存
  4. 在 640 秒时再次请求,不用等,得到 610 秒时刷新的缓存响应

可以看到我们在 640 秒时的这个请求,即不用等,也保证了新鲜度。实际上,我们在超过 max-age 的周期之后,在 stale-while-revalidate 指定的时间窗口之内发出的请求,都会得到这个作用。

如果没有恰好在那 30 秒内请求不还是没用么

说对了。600 和 30 这两个数值的搭配可能并不能让你想到实际使用场景,我们来举一个实例。

实例

假设我有一个 HTTP API,它返回现在是当前小时的第几分钟。它具有如下缓存控制响应头:

Cache-Control: max-age=1, stale-while-revalidate=59

如果在 1 秒钟内重新请求,那么分钟数和原来是一样的,之前缓存的结果完全是新鲜的;如果在 1-60 秒内请求,需要请求一个新的结果了,但原来的结果也不是不能用;如果在超过 60 秒后请求,则一定需要新的结果。

从这个例子可以看出,stale-while-revalidate 比较适用的场景是,我们查询的信息需要被刷新,但一定程度的陈旧是可以接受的。通常来讲,这种场景对应的业务是,我们请求的资源会在已知的或者可预见的周期内定时更新,同时我们会多次请求这个资源。在这样的场景下,stale-while-revalidate 可以在提供新鲜度有保证的响应结果的同时,减少重复请求的等待时间。

等等,这听起来有点像…

我们在开发应用时,为了减少请求的等待时间,减少空白页的出现,有时会在请求完成后,按照请求的 URL 等标识,将请求结果存储在 Redux/Vuex 等介质中,在页面上从 store 中读取数据来显示,而不是直接在 state 中维护和显示请求结果。这样一来,当需要重新请求相同的资源时,我们可以在界面上看到上次请求的结果,随后看到新的结果,减少空白页的出现。

如果你也有这样的操作,或者想要有这样的操作,不妨看看 zeit 的 swr 库,它已经帮你做了。swrstale-while-revalidate 的缩写,虽然得名于此,swr 只是借用了它的思想,实际实现与 stale-while-revalidate 指令并无关系。swr 在早读课的另一篇文章有介绍。这里不多赘述。

浏览器兼容性

stale-while-revalidate 不是缓存标准文档的一部分,而是扩展内容,属于实验性质。目前桌面浏览器只有 Chrome 75、Firefox 68、Edge 79 起对其有支持。

总结

缓存控制是应用优化中一个通用的、常用的方法,它不是单纯的前端技术或知识,也并非一夜之间就能一把梭的方法。很多时候缓存控制应该结合实际业务需求,对各个资源有针对性地使用,以在信息的准确性和响应的时间上达到最佳的平衡。

最后分享一句格言,它是 Google Web 指南里的一个章节标题。

Never load the same resource twice.

参考链接


Profile picture

Written by Doma who just migrated his blog to Gatsby.js. You should follow him on Twitter and GitHub.